/*
 * JSFConsole - Lightweight JSF Application Monitor
 * Copyright (C) 2009  Grzegorz Bugaj
 * http://www.gregbugaj.com
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*/
package com.gregbugaj.jsfconsole.agent;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.WeakHashMap;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.LoaderClassPath;
import javassist.NotFoundException;
/**
 * Intercept and monitor when/source of  FacesMessages  
 * @author devil
 *
 */
public class ValidationClassTransformer implements ClassFileTransformer {
	public static ClassPool classPool = new ClassPool(true);
	private static WeakHashMap<ClassLoader, Object> classLoaders = new WeakHashMap<ClassLoader, Object> ();


	/**
	 * Name of the map that contains information on how long did it tool for the class to run
	 */


	public byte[] transform(ClassLoader loader, String className,
			Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
			byte[] classfileBuffer) throws IllegalClassFormatException {

		// Check to see if we already know this class loader or add it to the Javassist ClassPool search path if
		// it's new. This is important in J2EE- and Servlet containers that have class loader hierarchies
		if (!classLoaders.containsKey(loader)) {        // <<< TODO: this is a weak hash map, will this really work as intended?
			classPool.insertClassPath(new LoaderClassPath(loader));
			classLoaders.put(loader, null);
		}

		if(!isCandidateClass(className)){
			return classfileBuffer;
		}		


		// Parses the byte array into a CtClass instance.
		CtClass ctc=null;
		ctc = loadClass(classfileBuffer, 0, classfileBuffer.length);
		if(ctc==null || ctc.isFrozen()){
			return classfileBuffer;
		}
		
		byte[] result=null;
		try {

		
			CtMethod[] methods=ctc.getMethods();
			for(CtMethod method:methods){
				System.out.println(method.getName());
				///addTimerWrapper(ctc, method);
			}
			
			addWrapper(ctc, ctc.getDeclaredMethod("addMessage"));
			
			//addMessage(clientId, msg)
			
			//addTimerWrapper(ctc, ctc.getDeclaredMethod("doPhase"));
			
			//addTimerWrapper(ctc, ctc.getDeclaredMethod("handleBeforePhase"));
			//addTimerWrapper(ctc, ctc.getDeclaredMethod("handleAfterPhase"));

			ctc.writeFile();
			result=ctc.toBytecode();
		} catch (NotFoundException e) {
			e.printStackTrace();
		} catch (CannotCompileException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

		return result;
	}


	private static CtClass loadClass(byte[] classfileBuffer, int offset, int length){
		CtClass cc=null;
		try{
			cc = classPool.makeClass(new ByteArrayInputStream(classfileBuffer, offset, length));
			if(cc.isFrozen()) {
				cc.defrost();
			}	
			//Inner class
			if(cc.getName().indexOf("$")>0){
				return null;
			}
			/*
			CtClass ccSuper=cc.getSuperclass();
			System.out.println(":::"+cc.getName());
			System.out.println(":::"+ccSuper.getName());
			
			System.out.println("-------------------");
			
			if(ccSuper!=null){
				if(ccSuper.getName().equals("java.lang.Object")){
					cc=null;
				}else 
					if(ccSuper.getName().equals("com.sun.faces.context") == false){									
						cc=null;
					}			
			}
			*/
		} catch (Exception e) {
			e.printStackTrace();
		}
		return cc;
	}


	private  void addWrapper(CtClass clas, CtMethod originalMethod) throws NotFoundException, CannotCompileException {
		if(originalMethod==null){
			System.out.println("Method is null for class"+clas);
			return;
		}
		//Original method signature addMessage(clientId, msg)
		String methodName=originalMethod.getName();
		//  rename old method to synthetic name, then duplicate the method with original name for use as interceptor
		String newMethodName = methodName+"$impl";
		originalMethod.setName(newMethodName);
		CtMethod newMethod = CtNewMethod.copy(originalMethod, methodName, clas, null);

		//  start the body text generation

		String type = originalMethod.getReturnType().getName();
		CtClass classType=originalMethod.getReturnType();

		StringBuffer body = new StringBuffer();
		body.append("{");
		//javax.faces.application.FacesMessage
		body.append("\n System.out.println(\"+++ VALID=\"+$1);");
		body.append("\n javax.faces.application.FacesMessage face = (javax.faces.application.FacesMessage)$2;\n");
		body.append("\n javax.faces.context.FacesContext face = javax.faces.context.FacesContext.getCurrentInstance();\n");
		body.append("\n Integer faceId = new Integer(face.hashCode()); \n");
		
		//We Track from where the event occured by throwing an Exception and then parsing the stacktrace 
		body.append("\n Exception exception=null; \n");
		body.append("\n try{throw new Exception();}catch(Exception ex){exception=ex;} \n");
		
		body.append("\n  com.gregbugaj.jsfconsole.agent.TraceEventManager.fireEvent(new  com.gregbugaj.jsfconsole.agent.TraceEvent( com.gregbugaj.jsfconsole.agent.EventType.VALIDATION_MESSAGE_ADDED, faceId, new Object[] {$1, $2, exception}));\n");


		if(classType!=CtClass.voidType){
			body.append(type + " result = ");
		}
		body.append(newMethodName + "($$);\n");

		//Return value if not void
		if(classType!=CtClass.voidType){
			body.append("return result;\n");
		}		
		
		body.append("}");

		System.out.println(body);
		//  replace the body of the interceptor method with generated  code block and add it to class
		newMethod.setBody(body.toString());
		clas.addMethod(newMethod);
	}

	/**
	 * Check if we should consider class for Instrumentation
	 * @param className
	 * @return
	 */
	private boolean isCandidateClass(String className) {
		if(className==null){
			return false;
		}
		//("javax/faces/context/FacesContext")
		boolean isCandidate=className.contains("FacesContextImpl");
		return isCandidate;
	}

}
